#!/usr/bin/python

"""
This script synchronizes a music collection recursivly one way from a losless format source directory to a lossy file format target directory.

It is for example usefull when you have a music collection in flac or wav format and want to keep a copy on a mp3player with the same file and directory structure but with mp3 files instead of the original flac files.

The source directory is walked recursivly:

If the filename exists on the target nothing is done (the file contents itself are not compared), otherwise files are converted:

* flac converted to mp3, id3 Tags are copied too
* wav converted to mp3
* ogg unchanged copied to target
* mp3 unchanged copied to target
* m3u unchanged copied to target
* Volume is adjusted with mp3gain

Missing directories on the target are created.
The type of file is determined by its extension.
The source and target directories must exist.


lame, flac, metaflac, mp3gain has to be installed
Tested on Debian Lenny.

Usage: music_collection_sync.py [options] source_dir target_dir

Options:
  -h, --help            show this help message and exit
  -l LOGLEVEL, --loglevel=LOGLEVEL
                        set's the log level (ie. the amount of output)
  -w, --target_win      convert the filenames to Windows convention (for example if you copy to a
                        FAT Partition)
                        
(c) 20.12.2009 Ing. Gregor Horvath, http://gregor-horvath.com, email:gh@gregor-horvath.com
This software is licensed under the GNU GPLv3 without any warranty as is.
"""

import os
import sys
import logging
from optparse import OptionParser

parser = OptionParser(usage="usage: %prog [options] source_dir target_dir")

parser.add_option("-l", "--loglevel", dest="loglevel", default="INFO",
                  help="set's the log level (ie. the amount of output)")

parser.add_option("-w", "--target_win", action="store_true", dest="win",
                  help="convert the filenames to Windows convention (for example if you copy to a FAT Partition)")

(options, args) = parser.parse_args()

if len(args) <> 2:
    parser.error("incorrect number of arguments")
try:
    level = int(getattr(logging, options.loglevel))
except (AttributeError, ValueError, TypeError):
    parser.error("incorrect loglevel value '%s'" % options.loglevel)

for path in args:
    if not os.path.exists(path):
        parser.error("path does not exist: %s" % path)

if args[0] == args[1]:
    parser.error("source and target path must not be the same")
    
if options.win:
    illegal_characters = '?[],\=+<>:;"*|^'
else:
    illegal_characters = ''
    
logging.basicConfig(level=level)

source_dir = args[0] 
target_dir = args[1] 

def flac_to_mp3(source, target):
    d = dict(title="''", tracknumber=0, genre="''", date="''", artist="''", album="''")
    d['source'] = shellquote(source)
    d['target'] = shellquote(target)
    id3_tags = os.popen("metaflac --export-tags-to=- %s" % shellquote(source)).read().split("\n")
    logging.debug("id3: %s" % id3_tags)
    for id3 in id3_tags:
        if id3:
            tag, value = id3.split("=", 1)
            d[tag.lower()] = shellquote(value)
        
    cmdstr = "flac -cd %(source)s | lame -b 256 -h --add-id3v2 "\
             "--tt %(title)s " \
             "--tn %(tracknumber)s " \
             "--tg %(genre)s "\
             "--ty %(date)s "\
             "--ta %(artist)s " \
             "--tl %(album)s " \
             "- %(target)s" % d
    logging.debug(cmdstr)
    os.system(cmdstr)
    # adjust volume with mp3gain
    os.system("mp3gain -r -c -d 10 %(target)s" % d)
    
def cp(source, target):
    os.system("cp %s %s" % (shellquote(source), shellquote(target)))

def wav_to_mp3(source, target):
    # TODO does it copy tags too?
    os.system("lame -h %s %s" % (shellquote(source), shellquote(target)))

def mkdir(source, target):
    os.system("mkdir %s" % shellquote(target))
    
convert_map = {"dir":["", mkdir],
               ".flac":[".mp3", flac_to_mp3],
               ".ogg":[".ogg", cp],
               ".mp3":[".mp3", cp],
               ".m3u":[".m3u", cp],
               ".wav":[".mp3", wav_to_mp3]}

def shellquote(s):
    return "'" + s.replace("'", "'\\''") + "'"

def convert(source_fname):
    target_fname = source_fname.replace(source_dir, target_dir)
    cmd = None
    if os.path.isfile(source_fname) or os.path.islink(source_fname):
        try:
            ext = os.path.splitext(source_fname)[1]
            conv = convert_map[ext]
        except KeyError:
            logging.warning("File extension '%s' not supported." % (ext))
        else:
            target_fname = os.path.splitext(target_fname)[0] + "%s" % conv[0]
            cmd = conv[1] 
    elif os.path.isdir(source_fname):
        cmd = convert_map["dir"][1] 
    else:        
        logging.error("File type not supported.")

    for c in illegal_characters:
        target_fname = target_fname.replace(c, "-")
        
    if not os.path.exists(target_fname):
        if cmd:
            logging.debug("cmd: %s, source: %s, target: %s" % (cmd, source_fname, target_fname))
            cmd(source_fname, target_fname)
        else:
            logging.debug("File '%s' ignored." % source_fname)
        
    else:
        logging.debug("Target '%s' already exists." % target_fname)

        
def do(arg, dirname, fnames):
    for f in fnames:
        source = os.path.join(dirname, f)
        convert(source)
        
os.path.walk(source_dir, do, None)
